package com.alibaba.dubbo.dynamic.service;

import com.alibaba.dubbo.common.bytecode.ClassGenerator;
import com.alibaba.dubbo.common.compiler.support.JavassistCompiler;
import com.alibaba.dubbo.common.logger.LoggerFactory;
import com.alibaba.dubbo.common.utils.ClassHelper;
import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.ProtocolConfig;
import com.alibaba.dubbo.config.RegistryConfig;
import com.alibaba.dubbo.config.spring.ServiceBean;
import com.alibaba.dubbo.dynamic.config.DynamicExportConfig;
import com.alibaba.dubbo.rpc.RpcException;
import javassist.ClassPool;
import javassist.LoaderClassPath;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.io.ClassPathResource;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

import static javassist.compiler.JvstCodeGen.clazzName;

/**
 * Created by wuyu on 2016/8/5.
 */
public class DynamicExportServiceImpl implements DynamicExportService, DisposableBean, ApplicationContextAware {

    private JavassistCompiler javassistCompiler = new JavassistCompiler();

    private ApplicationContext applicationContext;

    private Map<String, ServiceBean> serviceBeanMap = new ConcurrentHashMap<>();

    private Set<String> jars = new HashSet<>();

    private ClassPathResource jarTempPath = new ClassPathResource("app");


    public <T> Map<String, Object> export(List<String> jars, List<DynamicExportConfig> services) {
        Map<String, Object> map = new LinkedHashMap<>();
        if (services.size() == 0) {
            return map;
        }

        ClassLoader urlClassLoader = getUrlClassLoader(jars);
        Thread.currentThread().setContextClassLoader(loader);

        Class iFaceClazz = null;
        Class implClazz = null;


        for (DynamicExportConfig config : services) {
            String id = config.getId();
            String iFaceClazzName = config.getInterfaceName();
            String implClazzName = config.getServiceName();
            ServiceBean serviceBean = serviceBeanMap.get(id);
            if (serviceBean == null) {
                try {
                    iFaceClazz = Class.forName(iFaceClazzName, true, urlClassLoader);
                    implClazz = Class.forName(implClazzName, true, urlClassLoader);
                } catch (Exception e) {
                    map.put(id, e.getMessage());
                    continue;
                }
                T t = (T) register(id, implClazz);
                export(id, t, iFaceClazz, config);
                map.put(id, "success");
            }


        }
        return map;
    }


    private ClassLoader loader = null;

    public synchronized ClassLoader getUrlClassLoader(List<String> jarUrls) {
        if (jarUrls == null) {
            return loader;
        }
        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();


        if (loader == null) {
            loader = contextClassLoader;
        }

        for (int i = jarUrls.size() - 1; i <= 0; i++) {
            if (jars.contains(jarUrls.get(i))) {
                jarUrls.remove(i);
            } else {
                jars.add(jarUrls.get(i));
            }
        }

        URL[] urls = new URL[jarUrls.size()];

        for (int i = 0; i < jarUrls.size(); i++) {
            try {
                urls[i] = new URL(jarUrls.get(i));
            } catch (Exception e) {
                throw new RpcException(e);
            }
        }


        if (urls.length > 0) {
            loader = URLClassLoader.newInstance(urls, loader);
            ClassPool classPool = ClassGenerator.getClassPool(contextClassLoader);
            LoaderClassPath loaderClassPath = new LoaderClassPath(loader);
            classPool.insertClassPath(loaderClassPath);
        }

        return loader;
    }


    public Map<String, String> registerSpringBean(List<String> jars, Map<String, String> beanMap) {
        ClassLoader urlClassLoader = getUrlClassLoader(jars);
        Map<String, String> message = new HashMap<>();
        for (String id : beanMap.keySet()) {
            try {
                Class<?> aClass = urlClassLoader.loadClass(clazzName);
                register(id, aClass);
                message.put(id, "success");
            } catch (ClassNotFoundException e) {
                message.put(id, e.getMessage());
            }
        }
        return message;
    }


    public <T> T register(String id, Class<T> clazz) {
        ConfigurableApplicationContext application = getConfigurableApplicationContext();
        ConfigurableListableBeanFactory beanFactory = application.getBeanFactory();
        AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(clazz)
                .getBeanDefinition();
        DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) beanFactory;
        defaultListableBeanFactory.registerBeanDefinition(id, beanDefinition);
        Object bean = application.getBean(id);
        setBean(bean, application);
        beanFactory.configureBean(bean, id);
        beanFactory.applyBeanPostProcessorsBeforeInitialization(bean, id);
        beanFactory.applyBeanPostProcessorsAfterInitialization(bean, id);
        beanFactory.applyBeanPropertyValues(bean, id);
        beanFactory.initializeBean(bean, id);

        //兼容 xml set 方式配置的bean
        return (T) bean;
    }


    //用于兼容 xml bean set配置方式
    public void setBean(Object bean, ApplicationContext context) {

        Field[] declaredFields = bean.getClass().getDeclaredFields();
        List<String> beanNames = Arrays.asList(context.getBeanDefinitionNames());
        for (Field field : declaredFields) {
            String name = field.getName();
            try {
                Object o = field.get(bean);
                if (o == null && beanNames.contains(name)) {
                    Method[] methods = bean.getClass().getMethods();
                    for (Method method : methods) {
                        if (("set" + name).equalsIgnoreCase(method.getName()) && method.getParameterTypes().length == 1) {

                            Object fieldBean = context.getBean(name);
                            method.invoke(bean, fieldBean);
                        }
                    }
                }
            } catch (Exception e) {

            }
        }
    }


    private <T> void export(String id, T t, Class iFace, DynamicExportConfig config) {
        ServiceBean<T> serviceBean = new ServiceBean<T>();
        if (iFace == null) {
            Class<?>[] interfaces = t.getClass().getInterfaces();
            if (interfaces.length > 0) {
                serviceBean.setInterface(interfaces[0]);
            } else {
                throw new RpcException("the " + t.getClass().getName() + " Must implement the interface!");
            }
        } else {
            serviceBean.setInterface(iFace);
        }
        serviceBean.setApplicationContext(this.applicationContext);
        serviceBean.setRef(t);
        serviceBean.setDelay(1);
        serviceBean.setGroup(config.getGroup());
        serviceBean.setVersion(config.getVersion());
        serviceBean.setTimeout(config.getTimeout());
        serviceBean.setFilter(config.getFilter());

        try {
            serviceBean.afterPropertiesSet();
            serviceBeanMap.put(id, serviceBean);
        } catch (Exception e) {
            throw new RpcException(e);
        }
    }


    public void compile(String code) {
        javassistCompiler.compile(code, ClassHelper.getCallerClassLoader(getClass()));
    }

    @Override
    public void compileWithRegistererSpring(String code, String id) {
        register(id, javassistCompiler.compile(code, ClassHelper.getCallerClassLoader(getClass())));
    }

    private ConfigurableApplicationContext getConfigurableApplicationContext() {
        return (ConfigurableApplicationContext) this.applicationContext;
    }

    public List<Map<String, Object>> listDynamicBean() {
        List<Map<String, Object>> maps = new ArrayList<>();
        Map<String, Object> map = new LinkedHashMap<>();
        for (String id : serviceBeanMap.keySet()) {
            ServiceBean serviceBean = serviceBeanMap.get(id);
            ApplicationConfig application = serviceBean.getApplication();
            RegistryConfig registry = serviceBean.getRegistry();
            ProtocolConfig protocolConfig = serviceBean.getProtocol();
            map.put("id", id);
            map.put("service", serviceBean.getRef().getClass().getName());
            map.put("interface", serviceBean.getInterfaceClass().getName());
            map.put("application", application.getName());
            map.put("registry", registry.getAddress());
            map.put("group", serviceBean.getGroup());
            map.put("version", serviceBean.getVersion());
            map.put("timeout", serviceBean.getTimeout());
            map.put("filter", serviceBean.getFilter());
            List<com.alibaba.dubbo.common.URL> exportedUrls = serviceBean.getExportedUrls();
            for (com.alibaba.dubbo.common.URL url : exportedUrls) {
                String host = url.getHost();
                map.put("host", host);
                map.put("url", url.toServiceString());
            }
            map.put("protocol", protocolConfig.getName());
            map.put("port", protocolConfig.getPort());

            maps.add(map);
        }
        return maps;
    }


    public void unexport(List<String> ids) {
        for (String id : ids) {
            ServiceBean serviceBean = serviceBeanMap.remove(id);
            if (serviceBean != null) {
                ConfigurableListableBeanFactory beanFactory = getConfigurableApplicationContext().getBeanFactory();
                beanFactory.destroyBean(id, serviceBean);
            }
        }
    }

    public Map<String, Object> readLog(Long index, Integer limitLine) throws IOException {
        ArrayList<String> lines = new ArrayList<String>(limitLine);
        RandomAccessFile randomAccessFile = new RandomAccessFile(LoggerFactory.getFile().getAbsolutePath(), "r");
        randomAccessFile.seek(index);

        Long offset = index;
        for (int i = 0; i < limitLine; i++) {
            String s = randomAccessFile.readLine();
            if (s != null) {
                lines.add(s);
                offset += s.length();
            } else {
                break;
            }
        }
        Map<String, Object> map = new HashMap<>();
        map.put("lines", lines);
        map.put("length", randomAccessFile.length());
        map.put("offset", offset);
        randomAccessFile.close();
        return map;
    }


    public Map<String, Object> systemInfo() {
        Map map = new LinkedHashMap();
        Runtime r = Runtime.getRuntime();
        map.put("jvmTotal", r.totalMemory());// java总内存
        map.put("jvmUse", r.totalMemory() - r.freeMemory());// JVM使用内存
        map.put("jvmFree", r.freeMemory());// JVM剩余内存
        BigDecimal divide = new BigDecimal((r.totalMemory() - r.freeMemory())).divide(new BigDecimal(r.totalMemory()),
                2, 4);
        map.put("jvmUsage", divide.doubleValue());// JVM使用率
        map.put("cpu", r.availableProcessors());
        map.put("user.home", System.getProperty("user.home"));
        map.put("user.timezone", System.getProperty("user.timezone"));
        map.put("file.encoding", System.getProperty("file.encoding"));
        map.put("user.name", System.getProperty("user.name"));
        return map;
    }


    private Map<String, Integer> appMap = new HashMap<>();

    public Map<String, String> startSpringbootAppWithUrlClassLoader(String jarUrl, String appName, int port) {
        Map<String, String> message = new HashMap<>();
        String mainName = "org.springframework.boot.loader.JarLauncher";
        try {
            URL url = new URL(jarUrl);
            URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
            Class<?> aClass = urlClassLoader.loadClass(mainName);
            Method main = aClass.getMethod("main", String[].class);
            main.invoke(null, (Object) new String[]{"--server-port=" + port});
            appMap.put(appName, port);
            message.put(appName, "success");
            return message;
        } catch (Exception e) {
            message.put(appName, e.getMessage());
        }
        return message;
    }

    public Map<String, String> startSpringBootWithShell(String jarUrl, String appName, int port, String startCmd) {
        List<String> processList = new ArrayList<String>();
        try {
            Process process = Runtime.getRuntime().exec(startCmd);
            BufferedReader input = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line = "";
            while ((line = input.readLine()) != null) {
                processList.add(line);
            }
            input.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        for (String line : processList) {
            System.out.println(line);
        }
        return null;
    }


    public void destroy() throws Exception {
        for (String id : serviceBeanMap.keySet()) {
            ServiceBean serviceBean = serviceBeanMap.get(id);
            if (serviceBean != null) {
                serviceBean.unexport();
            }
        }
        serviceBeanMap.clear();
        applicationContext = null;
    }

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

}
